/**
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 * <p>
 * http://www.apache.org/licenses/LICENSE-2.0
 * <p>
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package com.dingmouren.paletteimageview.util;


public class ColorUtil {

    private static final int MIN_ALPHA_SEARCH_PRECISION = 1;
    private static final int MIN_ALPHA_SEARCH_MAX_ITERATIONS = 10;

    private static final ThreadLocal<double[]> TEMP_ARRAY = new ThreadLocal<>();

    public static int alpha(int color) {
        return (color >> 24) & 0xFF;
    }

    public static int red(int color) {
        return (color >> 16) & 0xFF;
    }

    public static int green(int color) {
        return (color >> 8) & 0xFF;
    }

    public static int blue(int color) {
        return color & 0xFF;
    }

    public static int rgb(int red, int green, int blue) {
        int color = (0xFF) << 8;
        color = (color | red) << 8;
        color = (color | green) << 8;
        color = (color | blue);
        return color;
    }

    public static int argb(int alpha, int red, int green, int blue) {
        int color = (alpha) << 8;
        color = (color | red) << 8;
        color = (color | green) << 8;
        color = (color | blue);
        return color;
    }

    public static int argb2rgba(int color) {
        return (color << 8) | (color >> 24 & 0xFF);
    }


    public static void RGB2HSL(float r, float g, float b, float[] hsl) {
        final double red = r / 255;
        final double green = g / 255;
        final double blue = b / 255;

        final double max = Math.max(red, Math.max(green, blue));
        final double min = Math.min(red, Math.min(green, blue));
        final double delta = max - min;

        double h, s;
        double l = (max + min) / 2;

        if (max == min) {
            h = s = 0;
        } else {
            if (max == red) {
                h = ((green - blue) / delta) % 6;
            } else if (max == green) {
                h = ((blue - red) / delta) + 2;
            } else {
                h = ((red - green) / delta) + 4;
            }

            s = delta / (1 - Math.abs(2 * l - 1));
        }

        h = (h * 60) % 360;
        if (h < 0) {
            h += 360;
        }

        hsl[0] = (float) constraint(h, 0, 360);
        hsl[1] = (float) constraint(s, 0, 1);
        hsl[2] = (float) constraint(l, 0, 1);

    }

    public static void color2HSL(int color, float[] hsl) {
        RGB2HSL(red(color), green(color), blue(color), hsl);
    }

    private static double constraint(double amount, double low, double high) {
        return amount < low ? low : (amount > high ? high : amount);
    }

    public static int HSL2Color(float[] hsl) {
        final double h = hsl == null ? 0 : hsl[0];
        final double s = hsl == null ? 0 : hsl[1];
        final double l = hsl == null ? 0 : hsl[2];

        final double c = (1 - Math.abs(2 * l - 1)) * s;
        final double m = l - 0.5f * c;
        final double x = c * (1 - Math.abs((h / 60 % 2) - 1));

        final int hueSegment = (int) h / 60;

        int r = 0, g = 0, b = 0;

        switch (hueSegment) {
            case 0:
                r = (int) Math.round(255 * (c + m));
                g = (int) Math.round(255 * (x + m));
                b = (int) Math.round(255 * m);
                break;
            case 1:
                r = (int) Math.round(255 * (x + m));
                g = (int) Math.round(255 * (c + m));
                b = (int) Math.round(255 * m);
                break;
            case 2:
                r = (int) Math.round(255 * m);
                g = (int) Math.round(255 * (c + m));
                b = (int) Math.round(255 * (x + m));
                break;
            case 3:
                r = (int) Math.round(255 * m);
                g = (int) Math.round(255 * (x + m));
                b = (int) Math.round(255 * (c + m));
                break;
            case 4:
                r = (int) Math.round(255 * (x + m));
                g = (int) Math.round(255 * m);
                b = (int) Math.round(255 * (c + m));
                break;
            case 5:
            case 6:
                r = (int) Math.round(255 * (c + m));
                g = (int) Math.round(255 * m);
                b = (int) Math.round(255 * (x + m));
                break;
        }

        r = constraint(r, 0, 255);
        g = constraint(g, 0, 255);
        b = constraint(b, 0, 255);

        return rgb(r, g, b);
    }

    private static int constraint(int amount, int low, int high) {
        return amount < low ? low : (amount > high ? high : amount);
    }

    public static int calculateMinimumAlpha(int foreground, int background, float minContrastRatio) {
//        if (alpha(background) != 255) {
//            throw new IllegalArgumentException("background can not be translucent: #" + Integer.toHexString(background));
//        }

        // First lets check that a fully opaque foreground has sufficient contrast
        int testForeground = setAlphaComponent(foreground, 255);
        double testRatio = calculateContrast(testForeground, background);
        if (testRatio < minContrastRatio) {
            // Fully opaque foreground does not have sufficient contrast, return error
            return -1;
        }
        int numIterations = 0;
        int minAlpha = 0;
        int maxAlpha = 255;

        while (numIterations <= MIN_ALPHA_SEARCH_MAX_ITERATIONS && (maxAlpha - minAlpha) > MIN_ALPHA_SEARCH_PRECISION) {
            final int testAlpha = (minAlpha + maxAlpha) / 2;

            testForeground = setAlphaComponent(foreground, testAlpha);
            testRatio = calculateContrast(testForeground, background);

            if (testRatio < minContrastRatio) {
                minAlpha = testAlpha;
            } else {
                maxAlpha = testAlpha;
            }

            numIterations++;
        }

        // Conservatively return the max of the range of possible alphas, which is known to pass.
        return maxAlpha;
    }

    public static int setAlphaComponent(int color, int alpha) {
//        if (alpha < 0 || alpha > 255) {
//            throw new IllegalArgumentException("alpha must be between 0 and 255.");
//        }
        return (color & 0x00ffffff) | (alpha << 24);
    }

    public static double calculateContrast(int foreground, int background) {
//        if (alpha(background) != 255) {
//            throw new IllegalArgumentException("background can not be translucent: #"
//                    + Integer.toHexString(background));
//        }
        if (alpha(foreground) < 255) {
            // If the foreground is translucent, composite the foreground over the background
            foreground = compositeColors(foreground, background);
        }

        final double luminance1 = calculateLuminance(foreground) + 0.05;
        final double luminance2 = calculateLuminance(background) + 0.05;

        // Now return the lighter luminance divided by the darker luminance
        return Math.max(luminance1, luminance2) / Math.min(luminance1, luminance2);
    }

    public static int compositeColors(int foreground, int background) {
        int bgAlpha = alpha(background);
        int fgAlpha = alpha(foreground);
        int a = compositeAlpha(fgAlpha, bgAlpha);

        int r = compositeComponent(red(foreground), fgAlpha, red(background), bgAlpha, a);
        int g = compositeComponent(green(foreground), fgAlpha, green(background), bgAlpha, a);
        int b = compositeComponent(blue(foreground), fgAlpha, blue(background), bgAlpha, a);

        return argb(a, r, g, b);
    }

    private static int compositeComponent(int fgC, int fgA, int bgC, int bgA, int a) {
        if (a == 0) return 0;
        return ((0xFF * fgC * fgA) + (bgC * bgA * (0xFF - fgA))) / (a * 0xFF);
    }

    private static int compositeAlpha(int foregroundAlpha, int backgroundAlpha) {
        return 0xFF - (((0xFF - backgroundAlpha) * (0xFF - foregroundAlpha)) / 0xFF);
    }

    public static double calculateLuminance(int color) {
        final double[] result = getTempDouble3Array();
        colorToXYZ(color, result);
        return result[1] / 100;
    }

    public static void RGBToXYZ(int r, int g, int b, double[] outXyz) {
        if (outXyz.length != 3) {
            throw new IllegalArgumentException("outXyz must have a length of 3.");
        }

        double sr = r / 255.0;
        sr = sr < 0.04045 ? sr / 12.92 : Math.pow((sr + 0.055) / 1.055, 2.4);
        double sg = g / 255.0;
        sg = sg < 0.04045 ? sg / 12.92 : Math.pow((sg + 0.055) / 1.055, 2.4);
        double sb = b / 255.0;
        sb = sb < 0.04045 ? sb / 12.92 : Math.pow((sb + 0.055) / 1.055, 2.4);

        outXyz[0] = 100 * (sr * 0.4124 + sg * 0.3576 + sb * 0.1805);
        outXyz[1] = 100 * (sr * 0.2126 + sg * 0.7152 + sb * 0.0722);
        outXyz[2] = 100 * (sr * 0.0193 + sg * 0.1192 + sb * 0.9505);
    }

    public static void colorToXYZ(int color, double[] outXyz) {
        RGBToXYZ(red(color), green(color), blue(color), outXyz);
    }

    private static double[] getTempDouble3Array() {
        double[] result = TEMP_ARRAY.get();
        if (result == null) {
            result = new double[3];
            TEMP_ARRAY.set(result);
        }
        return result;
    }
}
